/**
 * Copyright 2013 yangming.liu<liuyangming@gmail.com>.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, see <http://www.gnu.org/licenses/>.
 */
package org.bytesoft.openjta;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.transaction.HeuristicCommitException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;

import org.bytesoft.openjta.archive.TerminatorArchive;
import org.bytesoft.openjta.archive.TransactionArchive;
import org.bytesoft.openjta.archive.XAResourceArchive;
import org.bytesoft.openjta.coordinate.TransactionCoordinateContext;
import org.bytesoft.openjta.coordinate.TransactionCoordinator;
import org.bytesoft.openjta.coordinate.internal.LAOTransactionCoordinator;
import org.bytesoft.openjta.coordinate.internal.LROTransactionCoordinator;
import org.bytesoft.openjta.coordinate.internal.OPCTransactionCoordinator;
import org.bytesoft.openjta.coordinate.internal.RegularTransactionCoordinator;
import org.bytesoft.openjta.internal.RollbackRequiredException;
import org.bytesoft.openjta.internal.SynchronizationImpl;
import org.bytesoft.openjta.internal.TransactionVote;
import org.bytesoft.openjta.internal.XAResourceWrapper;
import org.bytesoft.openjta.resource.LocalTerminator;
import org.bytesoft.openjta.resource.RemoteTerminatorSkeleton;
import org.bytesoft.openjta.resource.Terminator;
import org.bytesoft.openjta.supports.TransactionLogger;
import org.bytesoft.openjta.supports.XAResourceMarshaller;
import org.bytesoft.openjta.xa.XidImpl;
import org.bytesoft.utils.CommonUtils;

public class TransactionImpl extends TransactionArchive implements Transaction {
	private static final long serialVersionUID = 1L;

	private int transactionTimeout;
	private final List<SynchronizationImpl> synchronizations = new ArrayList<SynchronizationImpl>();

	protected final RemoteTerminatorSkeleton skeleton = new RemoteTerminatorSkeleton(this);
	protected final LocalTerminator localTerminator = new LocalTerminator(this);

	protected boolean lroEnabled;
	protected boolean coordinateTaken;// TODO

	protected XAResourceMarshaller<Serializable> xaResourceMarshaller;
	protected TransactionCoordinateContext coordinateContext;

	protected TransactionManagerImpl transactionManager;
	protected TransactionLogger transactionLogger;

	public TransactionImpl() {
		this.transactionStatus = new TransactionStatus();
		this.transactionContext = new TransactionContext();

		this.transactionContext.setCoordinator(true);
		long createdTime = System.currentTimeMillis();
		long expiredTime = createdTime + (TransactionConstants.SECOND_MILLIS * this.transactionTimeout);
		this.transactionContext.setCreatedTime(createdTime);
		this.transactionContext.setExpiredTime(expiredTime);
	}

	public TransactionImpl(TransactionContext transactionContext) {
		this.transactionStatus = new TransactionStatus();
		this.transactionContext = transactionContext;

		if (transactionContext.containsNonXAResource()) {
			this.localResourceAllowed = false;
		}

	}

	@Override
	public synchronized void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException,
			SecurityException, SystemException {
		if (transactionStatus.isRollbackOnly()) {
			this.rollback();
			throw new HeuristicRollbackException();
		} else if (transactionStatus.isStatusRollingBack(true)) {
			this.rollback();
			throw new HeuristicRollbackException();
		} else if (transactionStatus.isRolledBack()) {
			throw new RollbackException();
		} else if (transactionStatus.isCommitted()) {
			// do nothing
		} else {
			try {
				this.processCommit();
				this.registerCleanup();
			} catch (HeuristicRollbackException hrex) {
				this.registerCleanup();
				throw hrex;
			} catch (RollbackException rex) {
				this.registerCleanup();
				throw rex;
			}
		}
	}

	public synchronized void processCommit() throws HeuristicMixedException, HeuristicRollbackException,
			RollbackException, SecurityException, SystemException {
		System.out.printf("[%s] commit%n", transactionContext.getXid());

		try {
			this.enterTransactionCompletion();
		} catch (RollbackRequiredException rrex) {
			this.rollback();
			throw new HeuristicRollbackException();
		} catch (RuntimeException rex) {
			// ignore
			rex.printStackTrace();
		}

		try {
			TransactionCoordinator coordinator = this.selectTransactionCoordinator();

			transactionStatus.transferToPreparing();
			this.transactionLogger.createTransaction(this);

			coordinator.processCommit(this.coordinateContext);
		} finally {
			this.handleAfterCompletion();
		}

	}

	@Override
	public synchronized void rollback() throws IllegalStateException, SystemException {
		if (this.transactionStatus.isCommitted()) {
			throw new IllegalStateException();
		} else if (this.transactionStatus.isRolledBack()) {
			return;
		} else {
			try {
				this.processRollback();
				this.registerCleanup();
			} catch (HeuristicMixedException ex) {
				SystemException exp = new SystemException();
				exp.initCause(ex);
				throw exp;
			} catch (HeuristicCommitException ex) {
				this.registerCleanup();

				SystemException exp = new SystemException();
				exp.initCause(ex);
				throw exp;
			}

		}
	}

	public synchronized void processRollback() throws HeuristicMixedException, HeuristicCommitException,
			SystemException {
		System.out.printf("[%s] rollback%n", transactionContext.getXid());

		try {
			this.enterTransactionCompletion();
		} catch (RollbackRequiredException ex) {
			// ignore
		} catch (RuntimeException ex) {
			// ignore
			ex.printStackTrace();
		}

		try {
			TransactionCoordinator coordinator = this.selectTransactionCoordinator();

			transactionStatus.transferToRollingback();
			this.transactionLogger.createTransaction(this);

			coordinator.processRollback(this.coordinateContext);
		} catch (RuntimeException ex) {
			SystemException sysex = new SystemException();
			sysex.initCause(ex);
			throw sysex;
		} finally {
			this.handleAfterCompletion();
		}

	}

	public synchronized int xaPrepare() throws SystemException, RollbackRequiredException {

		try {
			this.enterTransactionCompletion();
		} catch (RollbackRequiredException ex) {
			this.transactionStatus.markStatusRollback();
		} catch (RuntimeException ex) {
			// ignore
			ex.printStackTrace();
		}

		int vote = TransactionVote.XA_ROLLBACK_REQUIRED;
		try {
			TransactionCoordinator coordinator = this.selectTransactionCoordinator();

			this.transactionStatus.transferToPreparing();
			this.transactionLogger.createTransaction(this);

			vote = coordinator.xaPrepare(this.coordinateContext);

			this.transactionStatus.transferToPrepared();
		} finally {
			this.transactionLogger.completePreparePhase(this);
		}
		return vote;
	}

	public synchronized void xaCommit() throws SecurityException, HeuristicMixedException, HeuristicRollbackException,
			RollbackException, SystemException {
		this.transactionStatus.transferToCommitting();
		this.transactionLogger.enterCommitPhase(this);
		boolean success = false;
		boolean transactionCommitted = false;
		boolean transactionRolledback = false;
		try {
			TransactionCoordinator coordinator = this.selectTransactionCoordinator();

			coordinator.xaCommit(this.coordinateContext);

			transactionCommitted = true;
			success = true;
		} catch (HeuristicMixedException ex) {
			throw ex;
		} catch (HeuristicRollbackException ex) {
			success = true;
			transactionRolledback = true;
			throw ex;
		} catch (RollbackException ex) {
			success = true;
			transactionRolledback = true;
			throw ex;
		} catch (SecurityException ex) {
			throw ex;
		} catch (SystemException ex) {
			throw ex;
		} catch (RuntimeException ex) {
			SystemException sysexp = new SystemException();
			sysexp.initCause(ex);
			throw sysexp;
		} finally {
			this.handleAfterCompletion();
			if (success) {
				if (transactionCommitted) {
					this.transactionStatus.transferToCommitted();
					this.setCommitResult(true);
					this.transactionLogger.completeCommitPhase(this);
				} else if (transactionRolledback) {
					this.transactionStatus.transferToRolledback();
					this.setRollbackStarted(true);
					this.setRollbackResult(true);
					this.transactionLogger.completeRollbackPhase(this);
				}
			} else {
				this.transactionStatus.transferToCommitFail();
				this.setCommitResult(false);
				this.transactionLogger.completeCommitPhase(this);
			}// end-if-else
		}// end-finally
	}

	public synchronized void xaRollback() throws SecurityException, HeuristicMixedException, HeuristicCommitException,
			SystemException {
		this.transactionStatus.transferToRollingback();
		this.transactionLogger.enterRollbackPhase(this);
		boolean success = false;
		boolean transactionCommitted = false;
		boolean transactionRolledback = false;
		try {
			TransactionCoordinator coordinator = this.selectTransactionCoordinator();

			coordinator.xaRollback(this.coordinateContext);

			transactionRolledback = true;
			success = true;
		} catch (HeuristicMixedException ex) {
		} catch (HeuristicCommitException ex) {
			success = true;
			transactionCommitted = true;
		} catch (SystemException ex) {
		} catch (RuntimeException ex) {
		} finally {
			this.handleAfterCompletion();
			if (success) {
				if (transactionRolledback) {
					this.transactionStatus.transferToRolledback();
					this.setRollbackResult(true);
					this.transactionLogger.completeRollbackPhase(this);
				} else if (transactionCommitted) {
					this.transactionStatus.transferToCommitted();// TODO error
					this.setCommitStarted(true);
					this.setCommitResult(true);
					this.transactionLogger.completeCommitPhase(this);
				}
			} else {
				this.transactionStatus.transferToRollbackFail();
				this.setRollbackResult(false);
				this.transactionLogger.completeRollbackPhase(this);
			}// end-if-else
		}// end-finally
	}

	public void registerCleanup() {
		this.transactionManager.registerTransaction(this);
	}

	public void cleanup() throws SystemException {

		System.out.printf("[%s] cleanup%n", this.getGlobalXid());

		boolean terminatorCleanupFailureExists = false;
		Iterator<TerminatorArchive> itr = this.terminatorArchives.iterator();
		while (itr.hasNext()) {
			TerminatorArchive archive = itr.next();
			if (archive.isCleanupSuccess()) {
				// ignore
			} else {
				archive.setCleanupStarted(true);
				Terminator terminator = archive.getTerminator();
				try {
					terminator.cleanup();
					archive.setCleanupResult(true);
				} catch (RemoteException rex) {
					terminatorCleanupFailureExists = true;
				} catch (RuntimeException rex) {
					terminatorCleanupFailureExists = true;
				} finally {
					this.transactionLogger.cleanupTerminator(archive);
				}
			}
		}

		if (terminatorCleanupFailureExists) {
			throw new SystemException();
		}

		this.transactionManager.completeTransaction(this);
		this.transactionLogger.cleanupTransaction(this);

	}

	@Override
	public synchronized boolean enlistResource(XAResource xares) throws IllegalStateException, RollbackException,
			SystemException {
		if (transactionStatus.isActive()) {
			try {
				xares.setTransactionTimeout(this.transactionTimeout);
				XAResourceWrapper wrapper = null;
				if (XAResourceWrapper.class.isInstance(xares)) {
					wrapper = (XAResourceWrapper) xares;
				} else {
					boolean xaSupport = this.xaResourceMarshaller.xaSupport(xares);
					wrapper = new XAResourceWrapper(xares, xaSupport);
				}
				if (wrapper.isXaSupport()) {
					return localTerminator.enlistXAResource(wrapper);
				} else if (!this.lroEnabled) {
					throw new IllegalStateException();
				} else if (this.localResourceAllowed) {
					boolean result = localTerminator.enlistNonXAResource(wrapper);
					this.localResourceAllowed = false;
					this.nativeContainsLocalResource = true;
					this.transactionContext.setContainsNonXAResource(true);
					return result;
				} else if (this.nativeContainsLocalResource) {
					return localTerminator.enlistNonXAResource(wrapper);
				} else {
					throw new IllegalStateException();
				}
			} catch (XAException ex) {
				transactionStatus.markStatusRollback();
				SystemException exception = new SystemException();
				exception.initCause(ex);
				throw exception;
			} catch (RollbackException rex) {
				transactionStatus.markStatusRollback();
				throw rex;
			}
		} else if (transactionStatus.isRollbackOnly()) {
			throw new RollbackException();
		} else {
			throw new IllegalStateException();
		}
	}

	@Override
	public synchronized boolean delistResource(XAResource xares, int flags) throws IllegalStateException,
			SystemException {
		try {
			XAResourceWrapper wrapper = null;
			if (XAResourceWrapper.class.isInstance(xares)) {
				wrapper = (XAResourceWrapper) xares;
			} else {
				boolean xaSupport = this.xaResourceMarshaller.xaSupport(xares);
				wrapper = new XAResourceWrapper(xares, xaSupport);
			}
			if (wrapper.isXaSupport()) {
				return localTerminator.delistXAResource(wrapper, flags);
			} else {
				return localTerminator.delistNonXAResource(wrapper, flags);
			}
		} catch (RollbackRequiredException ex) {
			transactionStatus.markStatusRollback();
			return true;
		} catch (SystemException ex) {
			transactionStatus.markStatusRollback();
			throw ex;
		} catch (RuntimeException ex) {
			transactionStatus.markStatusRollback();
			throw ex;
		}
	}

	public void initialize() {
		localTerminator.setTransactionLogger(this.transactionLogger);
		if (transactionContext.isCoordinator()) {
			XidImpl globalXid = this.transactionManager.createGlobalXid();
			XidImpl branchXid = this.transactionManager.createBranchXid(globalXid);
			this.transactionContext.setXid(branchXid);
		}
	}

	protected TransactionCoordinator selectTransactionCoordinator() {

		if (this.coordinateContext == null) {
			this.coordinateContext = new TransactionCoordinateContext();
			this.coordinateContext.setTransactionContext(this.transactionContext);
			this.coordinateContext.setTransactionStatus(this.transactionStatus);
			this.coordinateContext.setTransactionLogger(this.transactionLogger);
			this.coordinateContext.setLocalTerminator(this.localTerminator);
			this.coordinateContext.setTerminatorArchives(this.terminatorArchives);
			this.coordinateContext.setTransactionArchive(this);

			TerminatorArchive lastTerminatorArchive = null;
			boolean remoteContainsLocalResource = false;
			int archiveNumber = this.terminatorArchives.size();
			for (int i = 0; i < archiveNumber; i++) {
				TerminatorArchive archive = this.terminatorArchives.get(i);
				if (archive.isContainsLocalResource()) {
					remoteContainsLocalResource = true;
					lastTerminatorArchive = archive;
					break;
				} else if (i == (archiveNumber - 1)) {
					lastTerminatorArchive = archive;
				}
			}

			boolean containsTerminator = this.terminatorArchives.size() > 0;
			if (nativeContainsLocalResource) {
				this.transactionMode = TransactionConstants.TX_MODE_LRO;

				System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "LRO");
			} else if (remoteContainsLocalResource) {
				this.transactionMode = TransactionConstants.TX_MODE_LAO;

				this.coordinateContext.setLastTerminatorArchive(lastTerminatorArchive);
				lastTerminatorArchive.setLastResource(true);

				System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "LAO");
			} else if (containsTerminator) {
				this.transactionMode = TransactionConstants.TX_MODE_LAO;

				this.coordinateContext.setLastTerminatorArchive(lastTerminatorArchive);
				lastTerminatorArchive.setLastResource(true);

				System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "LAO");
			} else {
				List<XAResourceArchive> allXAResourceArchives = this.localTerminator.getAllXAResourceArchives();

				boolean coordinator = transactionContext.isCoordinator();
				if (allXAResourceArchives.size() == 0) {
					this.coordinateContext.setTransactionLogger(TransactionLogger.defaultTransactionLogger);
					this.transactionLogger = TransactionLogger.defaultTransactionLogger;

					this.transactionMode = TransactionConstants.TX_MODE_NONE;

					System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "NONE");

				} else if (allXAResourceArchives.size() == 1 && (this.lroEnabled || coordinator)) {
					this.coordinateContext.setTransactionLogger(TransactionLogger.defaultTransactionLogger);
					this.transactionLogger = TransactionLogger.defaultTransactionLogger;

					this.transactionMode = TransactionConstants.TX_MODE_OPC;

					System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "OPC");
				} else if (allXAResourceArchives.size() == 1 && !this.lroEnabled) {
					this.transactionMode = TransactionConstants.TX_MODE_NONE;

					System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "NONE");
				} else if (this.lroEnabled) {
					this.transactionMode = TransactionConstants.TX_MODE_LRO;

					System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "LRO");
				} else {
					this.transactionMode = TransactionConstants.TX_MODE_LRO;

					System.out.printf("[%s] transaction-coordinate-mode: %s%n", this.getGlobalXid(), "NONE");
				}
			}
		}

		if (this.transactionMode == TransactionConstants.TX_MODE_NONE) {
			return new RegularTransactionCoordinator();
		} else if (this.transactionMode == TransactionConstants.TX_MODE_LRO) {
			return new LROTransactionCoordinator();
		} else if (this.transactionMode == TransactionConstants.TX_MODE_LAO) {
			return new LAOTransactionCoordinator();
		} else if (this.transactionMode == TransactionConstants.TX_MODE_OPC) {
			return new OPCTransactionCoordinator();
		} else {
			return new RegularTransactionCoordinator();
		}

	}

	public synchronized void registerTerminator(Terminator terminator, boolean terminatorContainsLocalResource) {
		TerminatorArchive archive = new TerminatorArchive();
		XidImpl branchXid = this.createBranchXid();
		archive.setBranchXid(branchXid);
		archive.setTerminator(terminator);
		if (this.terminatorArchives.contains(archive)) {
			Iterator<TerminatorArchive> itr = this.terminatorArchives.iterator();
			while (itr.hasNext()) {
				TerminatorArchive existedArchive = itr.next();
				if (archive.equals(existedArchive)) {
					archive.setContainsLocalResource(terminatorContainsLocalResource);
					break;
				}
			}
		} else {
			this.terminatorArchives.add(archive);
		}
	}

	@Override
	public synchronized void registerSynchronization(Synchronization sync) throws IllegalStateException,
			RollbackException, SystemException {
		if (sync == null) {
			// ignore
		} else if (transactionStatus.isActive() || transactionStatus.isRollbackOnly()) {
			this.synchronizations.add(new SynchronizationImpl(sync));
		} else {
			throw new IllegalStateException();
		}
	}

	protected void enterTransactionCompletion() throws RollbackRequiredException {
		this.handleBeforeCompletion();

		try {
			localTerminator.delistAllLocalResources();
		} catch (RollbackRequiredException ex) {
			throw ex;
		}
	}

	protected synchronized void handleBeforeCompletion() {
		Iterator<SynchronizationImpl> itr = this.synchronizations.iterator();
		while (itr.hasNext()) {
			SynchronizationImpl sync = itr.next();
			try {
				System.out.printf("[%s] before-completion: sync= %s%n", this.getGlobalXid(), sync);
				sync.beforeCompletion();
			} catch (RuntimeException ex) {
				// ignore
				ex.printStackTrace();
			}
		}
	}

	protected synchronized void handleAfterCompletion() {
		Iterator<SynchronizationImpl> itr = this.synchronizations.iterator();
		while (itr.hasNext()) {
			SynchronizationImpl sync = itr.next();
			try {
				System.out.printf("[%s] after-completion : sync= %s%n ", this.getGlobalXid(), sync);
				int status = transactionStatus.getCurrentStatus();
				sync.afterCompletion(status);
			} catch (RuntimeException ex) {
				// ignore
				ex.printStackTrace();
			}
		}
	}

	public XidImpl getGlobalXid() {
		XidImpl xid = this.transactionContext.getXid();
		return xid.getGlobalXid();
	}

	public XidImpl createBranchXid() {
		return this.transactionManager.createBranchXid(this.transactionContext);
	}

	@Override
	public int getStatus() throws SystemException {
		return transactionStatus.getCurrentStatus();
	}

	@Override
	public synchronized void setRollbackOnly() throws IllegalStateException, SystemException {
		transactionStatus.markStatusRollback();
	}

	public synchronized void suspendAllResource() throws IllegalStateException, SystemException {
		try {
			localTerminator.suspendAllResource();
		} catch (RollbackRequiredException e) {
			transactionStatus.markStatusRollback();
		}
	}

	public synchronized void resumeAllResource() throws IllegalStateException, SystemException {
		try {
			localTerminator.resumeAllResource();
		} catch (RollbackRequiredException e) {
			transactionStatus.markStatusRollback();
		}
	}

	@Override
	public int hashCode() {
		int hash = 23;
		XidImpl xid = this.transactionContext.getXid();
		hash += 29 * xid.hashCode();
		return hash;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		} else if (!TransactionImpl.class.equals(obj.getClass())) {
			return false;
		}

		TransactionImpl that = (TransactionImpl) obj;
		if (this.transactionContext == that.transactionContext) {
			return true;
		} else if (this.transactionContext == null || that.transactionContext == null) {
			return false;
		}
		XidImpl thisXid = this.transactionContext.getXid();
		XidImpl thatXid = that.transactionContext.getXid();

		return CommonUtils.equals(thisXid, thatXid);
	}

	@Override
	public List<XAResourceArchive> getXAResourceArchives() {
		return this.localTerminator.getAllXAResourceArchives();
	}

	public TransactionManagerImpl getTransactionManager() {
		return transactionManager;
	}

	public void setTransactionManager(TransactionManagerImpl transactionManager) {
		this.transactionManager = transactionManager;
	}

	public TransactionLogger getTransactionLogger() {
		return transactionLogger;
	}

	public void setTransactionLogger(TransactionLogger transactionLogger) {
		this.transactionLogger = transactionLogger;
	}

	public List<SynchronizationImpl> getSynchronizations() {
		return synchronizations;
	}

	public LocalTerminator getLocalTerminator() {
		return this.localTerminator;
	}

	public RemoteTerminatorSkeleton getSkeleton() {
		return skeleton;
	}

	public XAResourceMarshaller<Serializable> getXaResourceMarshaller() {
		return xaResourceMarshaller;
	}

	public void setXaResourceMarshaller(XAResourceMarshaller<Serializable> xaResourceMarshaller) {
		this.xaResourceMarshaller = xaResourceMarshaller;
	}

	public int getTransactionTimeout() {
		return transactionTimeout;
	}

	public void setTransactionTimeout(int transactionTimeout) {
		this.transactionTimeout = transactionTimeout;
	}

	public void setLroEnabled(boolean lroEnabled) {
		this.lroEnabled = lroEnabled;
	}

	public boolean isCoordinateTaken() {
		return coordinateTaken;
	}

	public void setCoordinateTaken(boolean coordinateTaken) {
		this.coordinateTaken = coordinateTaken;
	}

	public boolean isFresh() {
		return TransactionImpl.class.equals(this.getClass());
	}

}
